Lambda PowertoolsのParameters utilityで独自Providerを実装してGoogle Cloudからシークレット値を取得してみた
リテールアプリ共創部@大阪の岩田です。
AWS Lambda Powertools for TypeScriptのParameters utilityは様々なパラメータストアからパラメータ値を取得するための高レベルなAPIを提供しています。デフォルトだとパラメータの取得元として以下のProviderが提供されており、それぞれ対応するパラメータストアから簡単にパラメータ値が取得できるようになっています。
- SSMProvider
- SecretsProvider
- AppConfigProvider
- DynamoDBProvider
また、これらのProviderに加えて独自のProviderを定義するための枠組みも用意されており、決められたIFに沿って独自のProviderを実装すれば前述したパラメータストア以外にも任意のパラメータストアからパラメータ値が取得可能です。
このブログではミニマムな独自Providerを実装し、Google CloudのSecretManagerからシークレット値を取得してみます。
環境
今回利用した環境です
- Node.js: v20.11.1
- @aws-lambda-powertools/parameters: 2.7.0
- @google-cloud/secret-manager: 5.6.0
やってみる
独自Providerの実装
コード全体は以下のようになりました
import { BaseProvider } from "@aws-lambda-powertools/parameters/base";
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
import type * as gax from "google-gax";
import type { ClientOptions } from "google-gax";
class GcloudSecretManagerServiceClient extends SecretManagerServiceClient {
// SecretManagerServiceClientをそのまま使うと
// `Failed to add user agent middleware Error: The client provided does not match the expected interface`
// のwarnログが出力される
// warnログ抑制のため期待されるinterfaceのダミー実装を組み込む
public middlewareStack: object;
public config: object;
constructor(
opts?: ClientOptions,
gaxInstance?: typeof gax | typeof gax.fallback,
) {
super(opts, gaxInstance);
this.config = {};
this.middlewareStack = {
identify: () => [],
addRelativeTo: () => {},
};
}
send(): void {}
}
export class GcloudSecretManagerProvider extends BaseProvider {
public declare client: GcloudSecretManagerServiceClient;
public constructor() {
super({
proto:
GcloudSecretManagerServiceClient as new () => GcloudSecretManagerServiceClient,
});
}
protected async _get(name: string): Promise<string> {
const [res] = await this.client.accessSecretVersion({ name });
return res.payload?.data?.toString() ?? "";
}
protected async _getMultiple(
_path: string,
_options?: unknown,
): Promise<Record<string, unknown> | undefined> {
throw new Error("Method not implemented.");
}
}
ポイントを解説していきます。
まずSecretManagerServiceClientを継承したGcloudSecretManagerServiceClientというクラスを定義しています
class GcloudSecretManagerServiceClient extends SecretManagerServiceClient {
// SecretManagerServiceClientをそのまま使うと
// `Failed to add user agent middleware Error: The client provided does not match the expected interface`
// のwarnログが出力される
// warnログ抑制のため期待されるinterfaceのダミー実装を組み込む
public middlewareStack: object;
public config: object;
constructor(
opts?: ClientOptions,
gaxInstance?: typeof gax | typeof gax.fallback,
) {
super(opts, gaxInstance);
this.config = {};
this.middlewareStack = {
identify: () => [],
addRelativeTo: () => {},
};
}
send(): void {}
}
このクラスを定義するのは必須では無いですが、独自Providerの内部で利用するクラスがconfig
やmiddlewareStack
というフィールドを持っていないとFailed to add user agent middleware Error: The client provided does not match the expected interface
というwarnログが出力されてうっとおしいので、ダミーのフィールドを定義しています。
ちなみにwarnログを出力している処理はこのあたりですisSdkClient
のチェックがfalseの場合にwarnログが出力されます。
isSdkClient
の実装はこちらです。このチェック処理に合わせてダミーのフィールドを定義してやればwarnログの出力が回避できます。
メインとなる独自Providerの実装です
export class GcloudSecretManagerProvider extends BaseProvider {
public declare client: GcloudSecretManagerServiceClient;
public constructor() {
super({
proto:
GcloudSecretManagerServiceClient as new () => GcloudSecretManagerServiceClient,
});
}
public async get(name: string): Promise<Record<string, unknown> | undefined> {
return super.get(name) as Promise<Record<string, unknown> | undefined>;
}
public async getMultiple(path: string, _options?: unknown): Promise<void> {
await super.getMultiple(path);
}
protected async _get(name: string): Promise<string> {
const [res] = await this.client.accessSecretVersion({ name });
return res.payload?.data?.toString() ?? "";
}
protected async _getMultiple(
_path: string,
_options?: unknown,
): Promise<Record<string, unknown> | undefined> {
throw new Error("Method not implemented.");
}
}
まずconstructor
ですが、ここで基底クラスのコンストラクタを呼び出してGcloudSecretManagerServiceClient
のインスタンスを生成します。proto
の型がnew (config?: unknown) => unknown
なので、as new () => GcloudSecretManagerServiceClient
を付けてGcloudSecretManagerServiceClient
のコンストラクタによってGcloudSecretManagerServiceClient
が生成されることを明示します。
続いて_get
に基底クラスの_get
をオーバーライドして独自のパラメータ値取得処理を実装します。コンストラクタでthis.client
にGcloudSecretManagerServiceClient
のインスタンスがセットされているので、this.client.accessSecretVersion({ name })
でGoogle CloudのSecret Managerからシークレット値を取得して値を返却します。
_getMultiple
に関しては今回はちゃんと実装しないので、Errorをthrowするだけの簡易な実装を入れて型エラーを回避しています。
独自Providerを使ってみる
独自Providerの準備ができたので実際に使ってみます。以下のコードで独自Providerを使ったシークレット値が取得可能です。
import { GcloudSecretManagerProvider } from "./gcloud-secret-manager-provider";
const main = async () => {
const secretName =
"projects/<プロジェクトNo>/secrets/<シークレット名>/versions/latest";
const secretsProvider = new GcloudSecretManagerProvider();
const secret = await secretsProvider.get(secretName);
console.log(`secret: ${secret}`);
};
main();
実際に動かしてみると...
$npx ts-node index.ts
secret: <Google Cloudから取得したシークレット値>
無事にシークレット値が出力されました!!
まとめ
AWS Lambda Powertools for TypeScriptのParameters utilityで独自のProviderを実装するサンプルをご紹介しました。今回はミニマムな実装としていますが、基底クラスであるBaseProviderは継承先で各種処理を拡張することで様々なオプションを処理できるように設計されています。デフォルトでは非対応のパラメータストアから値を取得しつつ、Powertoolsの便利機能の恩恵を受けたい場合は独自Providerの実装も検討してみてください。